package org.apache.maven.plugin.war;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import org.apache.maven.archiver.MavenArchiveConfiguration;
import org.apache.maven.artifact.factory.ArtifactFactory;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.Resource;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugin.war.overlay.OverlayManager;
import org.apache.maven.plugin.war.packaging.DependenciesAnalysisPackagingTask;
import org.apache.maven.plugin.war.packaging.OverlayPackagingTask;
import org.apache.maven.plugin.war.packaging.SaveWebappStructurePostPackagingTask;
import org.apache.maven.plugin.war.packaging.WarPackagingContext;
import org.apache.maven.plugin.war.packaging.WarPackagingTask;
import org.apache.maven.plugin.war.packaging.WarPostPackagingTask;
import org.apache.maven.plugin.war.packaging.WarProjectPackagingTask;
import org.apache.maven.plugin.war.util.WebappStructure;
import org.apache.maven.plugin.war.util.WebappStructureSerializer;
import org.apache.maven.project.MavenProject;
import org.apache.maven.shared.filtering.MavenFileFilter;
import org.apache.maven.shared.filtering.MavenFilteringException;
import org.apache.maven.shared.filtering.MavenResourcesExecution;
import org.apache.maven.shared.filtering.MavenResourcesFiltering;
import org.codehaus.plexus.archiver.jar.JarArchiver;
import org.codehaus.plexus.archiver.manager.ArchiverManager;
import org.codehaus.plexus.util.StringUtils;

/**
 * Contains common jobs for WAR mojos.
 * 
 * @version $Id: AbstractWarMojo.java 1235053 2012-01-23 23:03:53Z dennisl $
 */
public abstract class AbstractWarMojo extends AbstractMojo {
	public static final String DEFAULT_FILE_NAME_MAPPING = "@{artifactId}@-@{version}@.@{extension}@";

	public static final String DEFAULT_FILE_NAME_MAPPING_CLASSIFIER = "@{artifactId}@-@{version}@-@{classifier}@.@{extension}@";

	private static final String[] EMPTY_STRING_ARRAY = {};

	private static final String META_INF = "META-INF";

	private static final String WEB_INF = "WEB-INF";

	/**
	 * The Maven project.
	 * 
	 * @parameter default-value="${project}"
	 * @required
	 * @readonly
	 */
	private MavenProject project;

	/**
	 * The directory containing compiled classes.
	 * 
	 * @parameter default-value="${project.build.outputDirectory}"
	 * @required
	 * @readonly
	 */
	private File classesDirectory;

	/**
	 * Whether a JAR file will be created for the classes in the webapp. Using
	 * this optional configuration parameter will make the compiled classes to
	 * be archived into a JAR file and the classes directory will then be
	 * excluded from the webapp.
	 * 
	 * @parameter expression="${archiveClasses}" default-value="false"
	 * @since 2.0.1
	 */
	private boolean archiveClasses;

	/**
	 * The JAR archiver needed for archiving the classes directory into a JAR
	 * file under WEB-INF/lib.
	 * 
	 * @component role="org.codehaus.plexus.archiver.Archiver" role-hint="jar"
	 * @required
	 */
	private JarArchiver jarArchiver;

	/**
	 * The directory where the webapp is built.
	 * 
	 * @parameter 
	 *            default-value="${project.build.directory}/${project.build.finalName}"
	 * @required
	 */
	private File webappDirectory;

	/**
	 * Single directory for extra files to include in the WAR. This is where you
	 * place your JSP files.
	 * 
	 * @parameter default-value="${basedir}/src/main/webapp"
	 * @required
	 */
	private File warSourceDirectory;

	/**
	 * The list of webResources we want to transfer.
	 * 
	 * @parameter
	 */
	private Resource[] webResources;

	/**
	 * Filters (property files) to include during the interpolation of the
	 * pom.xml.
	 * 
	 * @parameter
	 */
	private List filters;

	/**
	 * The path to the web.xml file to use.
	 * 
	 * @parameter expression="${maven.war.webxml}"
	 */
	private File webXml;

	/**
	 * The path to a configuration file for the servlet container. Note that the
	 * file name may be different for different servlet containers. Apache
	 * Tomcat uses a configuration file named context.xml. The file will be
	 * copied to the META-INF directory.
	 * 
	 * @parameter expression="${maven.war.containerConfigXML}"
	 */
	private File containerConfigXML;

	/**
	 * Directory to unpack dependent WARs into if needed.
	 * 
	 * @parameter default-value="${project.build.directory}/war/work"
	 * @required
	 */
	private File workDirectory;

	/**
	 * The file name mapping to use when copying libraries and TLDs. If no file
	 * mapping is set (default) the files are copied with their standard names.
	 * 
	 * @parameter
	 * @since 2.1-alpha-1
	 */
	private String outputFileNameMapping;

	/**
	 * The file containing the webapp structure cache.
	 * 
	 * @parameter 
	 *            default-value="${project.build.directory}/war/work/webapp-cache.xml"
	 * @required
	 * @since 2.1-alpha-1
	 */
	private File cacheFile;

	/**
	 * Whether the cache should be used to save the status of the webapp across
	 * multiple runs. Experimental feature so disabled by default.
	 * 
	 * @parameter expression="${useCache}" default-value="false"
	 * @since 2.1-alpha-1
	 */
	private boolean useCache = false;

	/**
	 * @component role="org.apache.maven.artifact.factory.ArtifactFactory"
	 * @required
	 * @readonly
	 */
	private ArtifactFactory artifactFactory;

	/**
	 * To look up Archiver/UnArchiver implementations.
	 * 
	 * @component role="org.codehaus.plexus.archiver.manager.ArchiverManager"
	 * @required
	 */
	private ArchiverManager archiverManager;

	/**
	 * @component role="org.apache.maven.shared.filtering.MavenFileFilter"
	 *            role-hint="default"
	 * @required
	 */
	private MavenFileFilter mavenFileFilter;

	/**
	 * @component 
	 *            role="org.apache.maven.shared.filtering.MavenResourcesFiltering"
	 *            role-hint="default"
	 * @required
	 */
	private MavenResourcesFiltering mavenResourcesFiltering;

	/**
	 * The comma separated list of tokens to include when copying the content of
	 * the warSourceDirectory.
	 * 
	 * @parameter alias="includes" default-value="**"
	 */
	private String warSourceIncludes;

	/**
	 * The comma separated list of tokens to exclude when copying the content of
	 * the warSourceDirectory.
	 * 
	 * @parameter alias="excludes"
	 */
	private String warSourceExcludes;

	/**
	 * The comma separated list of tokens to include when doing a WAR overlay.
	 * Default is '**'
	 * 
	 * @parameter
	 * @deprecated Use &lt;overlay&gt;/&lt;includes&gt; instead
	 */
	private String dependentWarIncludes = "**/**";

	/**
	 * The comma separated list of tokens to exclude when doing a WAR overlay.
	 * 
	 * @parameter
	 * @deprecated Use &lt;overlay&gt;/&lt;excludes&gt; instead
	 */
	private String dependentWarExcludes = "META-INF/**";

	/**
	 * The overlays to apply.
	 * 
	 * @parameter
	 * @since 2.1-alpha-1
	 */
	private List overlays = new ArrayList();

	/**
	 * A list of file extensions that should not be filtered. <b>Will be used
	 * when filtering webResources and overlays.</b>
	 * 
	 * @parameter
	 * @since 2.1-alpha-2
	 */
	private List nonFilteredFileExtensions;

	/**
	 * @parameter default-value="${session}"
	 * @readonly
	 * @required
	 * @since 2.1-alpha-2
	 */
	private MavenSession session;

	/**
	 * To filter deployment descriptors. <b>Disabled by default.</b>
	 * 
	 * @parameter expression="${maven.war.filteringDeploymentDescriptors}"
	 *            default-value="false"
	 * @since 2.1-alpha-2
	 */
	private boolean filteringDeploymentDescriptors = false;

	/**
	 * To escape interpolated values with Windows path <code>c:\foo\bar</code>
	 * will be replaced with <code>c:\\foo\\bar</code>.
	 * 
	 * @parameter expression="${maven.war.escapedBackslashesInFilePath}"
	 *            default-value="false"
	 * @since 2.1-alpha-2
	 */
	private boolean escapedBackslashesInFilePath = false;

	/**
	 * Expression preceded with this String won't be interpolated.
	 * <code>\${foo}</code> will be replaced with <code>${foo}</code>.
	 * 
	 * @parameter expression="${maven.war.escapeString}"
	 * @since 2.1-beta-1
	 */
	protected String escapeString;

	/**
	 * The archive configuration to use. See <a
	 * href="http://maven.apache.org/shared/maven-archiver/index.html">Maven
	 * Archiver Reference</a>.
	 * 
	 * @parameter
	 */
	private MavenArchiveConfiguration archive = new MavenArchiveConfiguration();

	private final WebappStructureSerializer webappStructureSerialier = new WebappStructureSerializer();

	private final Overlay currentProjectOverlay = Overlay.createInstance();

	public Overlay getCurrentProjectOverlay() {
		return currentProjectOverlay;
	}

	/**
	 * Returns a string array of the excludes to be used when copying the
	 * content of the WAR source directory.
	 * 
	 * @return an array of tokens to exclude
	 */
	protected String[] getExcludes() {
		List excludeList = new ArrayList();
		if (StringUtils.isNotEmpty(warSourceExcludes)) {
			excludeList.addAll(Arrays.asList(StringUtils.split(warSourceExcludes, ",")));
		}

		// if webXML is specified, omit the one in the source directory
		if (webXml != null && StringUtils.isNotEmpty(webXml.getName())) {
			excludeList.add("**/" + WEB_INF + "/web.xml");
		}

		// if contextXML is specified, omit the one in the source directory
		if (containerConfigXML != null && StringUtils.isNotEmpty(containerConfigXML.getName())) {
			excludeList.add("**/" + META_INF + "/" + containerConfigXML.getName());
		}

		return (String[]) excludeList.toArray(EMPTY_STRING_ARRAY);
	}

	/**
	 * Returns a string array of the includes to be used when assembling/copying
	 * the WAR.
	 * 
	 * @return an array of tokens to include
	 */
	protected String[] getIncludes() {
		return StringUtils.split(StringUtils.defaultString(warSourceIncludes), ",");
	}

	/**
	 * Returns a string array of the excludes to be used when adding dependent
	 * WAR as an overlay onto this WAR.
	 * 
	 * @return an array of tokens to exclude
	 */
	protected String[] getDependentWarExcludes() {
		String[] excludes;
		if (StringUtils.isNotEmpty(dependentWarExcludes)) {
			excludes = StringUtils.split(dependentWarExcludes, ",");
		} else {
			excludes = EMPTY_STRING_ARRAY;
		}
		return excludes;
	}

	/**
	 * Returns a string array of the includes to be used when adding dependent
	 * WARs as an overlay onto this WAR.
	 * 
	 * @return an array of tokens to include
	 */
	protected String[] getDependentWarIncludes() {
		return StringUtils.split(StringUtils.defaultString(dependentWarIncludes), ",");
	}

	public void buildExplodedWebapp(File webappDirectory) throws MojoExecutionException, MojoFailureException {
		webappDirectory.mkdirs();

		try {
			buildWebapp(project, webappDirectory);
		} catch (IOException e) {
			throw new MojoExecutionException("Could not build webapp", e);
		}
	}

	/**
	 * Builds the webapp for the specified project with the new packaging task
	 * thingy
	 * <p/>
	 * Classes, libraries and tld files are copied to the
	 * <tt>webappDirectory</tt> during this phase.
	 * 
	 * @param project
	 *            the maven project
	 * @param webappDirectory
	 *            the target directory
	 * @throws MojoExecutionException
	 *             if an error occurred while packaging the webapp
	 * @throws MojoFailureException
	 *             if an unexpected error occurred while packaging the webapp
	 * @throws IOException
	 *             if an error occurred while copying the files
	 */
	public void buildWebapp(MavenProject project, File webappDirectory) throws MojoExecutionException,
			MojoFailureException, IOException {

		WebappStructure cache;
		if (useCache && cacheFile.exists()) {
			cache = new WebappStructure(project.getDependencies(), webappStructureSerialier.fromXml(cacheFile));
		} else {
			cache = new WebappStructure(project.getDependencies(), null);
		}

		final long startTime = System.currentTimeMillis();
		getLog().info("Assembling webapp [" + project.getArtifactId() + "] in [" + webappDirectory + "]");

		final OverlayManager overlayManager = new OverlayManager(overlays, project, dependentWarIncludes,
				dependentWarExcludes, currentProjectOverlay);
		final List packagingTasks = getPackagingTasks(overlayManager);
		List defaultFilterWrappers = null;
		try {
			MavenResourcesExecution mavenResourcesExecution = new MavenResourcesExecution();
			mavenResourcesExecution.setEscapeString(escapeString);

			defaultFilterWrappers = mavenFileFilter.getDefaultFilterWrappers(project, filters,
					escapedBackslashesInFilePath, this.session, mavenResourcesExecution);

		} catch (MavenFilteringException e) {
			getLog().error("fail to build filering wrappers " + e.getMessage());
			throw new MojoExecutionException(e.getMessage(), e);
		}

		final WarPackagingContext context = new DefaultWarPackagingContext(webappDirectory, cache, overlayManager,
				defaultFilterWrappers, getNonFilteredFileExtensions(), filteringDeploymentDescriptors,
				this.artifactFactory);
		final Iterator it = packagingTasks.iterator();
		while (it.hasNext()) {
			WarPackagingTask warPackagingTask = (WarPackagingTask) it.next();
			warPackagingTask.performPackaging(context);
		}

		// Post packaging
		final List postPackagingTasks = getPostPackagingTasks();
		final Iterator it2 = postPackagingTasks.iterator();
		while (it2.hasNext()) {
			WarPostPackagingTask task = (WarPostPackagingTask) it2.next();
			task.performPostPackaging(context);

		}
		getLog().info("Webapp assembled in [" + (System.currentTimeMillis() - startTime) + " msecs]");

	}

	/**
	 * Returns a <tt>List</tt> of the
	 * {@link org.apache.maven.plugin.war.packaging.WarPackagingTask} instances
	 * to invoke to perform the packaging.
	 * 
	 * @param overlayManager
	 *            the overlay manager
	 * @return the list of packaging tasks
	 * @throws MojoExecutionException
	 *             if the packaging tasks could not be built
	 */
	private List getPackagingTasks(OverlayManager overlayManager) throws MojoExecutionException {
		final List packagingTasks = new ArrayList();
		if (useCache) {
			packagingTasks.add(new DependenciesAnalysisPackagingTask());
		}

		final List resolvedOverlays = overlayManager.getOverlays();
		final Iterator it = resolvedOverlays.iterator();
		while (it.hasNext()) {
			Overlay overlay = (Overlay) it.next();
			if (overlay.isCurrentProject()) {
				packagingTasks.add(new WarProjectPackagingTask(webResources, webXml, containerConfigXML,
						currentProjectOverlay));
			} else {
				packagingTasks.add(new OverlayPackagingTask(overlay, currentProjectOverlay));
			}
		}
		return packagingTasks;
	}

	/**
	 * Returns a <tt>List</tt> of the
	 * {@link org.apache.maven.plugin.war.packaging.WarPostPackagingTask}
	 * instances to invoke to perform the post-packaging.
	 * 
	 * @return the list of post packaging tasks
	 */
	private List getPostPackagingTasks() {
		final List postPackagingTasks = new ArrayList();
		if (useCache) {
			postPackagingTasks.add(new SaveWebappStructurePostPackagingTask(cacheFile));
		}
		// TODO add lib scanning to detect duplicates
		return postPackagingTasks;
	}

	/**
	 * WarPackagingContext default implementation
	 */
	private class DefaultWarPackagingContext implements WarPackagingContext {

		private final ArtifactFactory artifactFactory;

		private final WebappStructure webappStructure;

		private final File webappDirectory;

		private final OverlayManager overlayManager;

		private final List filterWrappers;

		private List nonFilteredFileExtensions;

		private boolean filteringDeploymentDescriptors;

		public DefaultWarPackagingContext(File webappDirectory, final WebappStructure webappStructure,
				final OverlayManager overlayManager, List filterWrappers, List nonFilteredFileExtensions,
				boolean filteringDeploymentDescriptors, ArtifactFactory artifactFactory) {
			this.webappDirectory = webappDirectory;
			this.webappStructure = webappStructure;
			this.overlayManager = overlayManager;
			this.filterWrappers = filterWrappers;
			this.artifactFactory = artifactFactory;
			this.filteringDeploymentDescriptors = filteringDeploymentDescriptors;
			this.nonFilteredFileExtensions = nonFilteredFileExtensions == null ? Collections.EMPTY_LIST
					: nonFilteredFileExtensions;
			// This is kinda stupid but if we loop over the current overlays and
			// we request the path structure
			// it will register it. This will avoid wrong warning messages in a
			// later phase
			final Iterator it = overlayManager.getOverlayIds().iterator();
			while (it.hasNext()) {
				String overlayId = (String) it.next();
				webappStructure.getStructure(overlayId);
			}
		}

		public MavenProject getProject() {
			return project;
		}

		public File getWebappDirectory() {
			return webappDirectory;
		}

		public File getClassesDirectory() {
			return classesDirectory;
		}

		public Log getLog() {
			return AbstractWarMojo.this.getLog();
		}

		public String getOutputFileNameMapping() {
			return outputFileNameMapping;
		}

		public File getWebappSourceDirectory() {
			return warSourceDirectory;
		}

		public String[] getWebappSourceIncludes() {
			return getIncludes();
		}

		public String[] getWebappSourceExcludes() {
			return getExcludes();
		}

		public boolean archiveClasses() {
			return archiveClasses;
		}

		public File getOverlaysWorkDirectory() {
			return workDirectory;
		}

		public ArchiverManager getArchiverManager() {
			return archiverManager;
		}

		public MavenArchiveConfiguration getArchive() {
			return archive;
		}

		public JarArchiver getJarArchiver() {
			return jarArchiver;
		}

		public List getFilters() {
			return filters;
		}

		public WebappStructure getWebappStructure() {
			return webappStructure;
		}

		public List getOwnerIds() {
			return overlayManager.getOverlayIds();
		}

		public MavenFileFilter getMavenFileFilter() {
			return mavenFileFilter;
		}

		public List getFilterWrappers() {
			return filterWrappers;
		}

		public boolean isNonFilteredExtension(String fileName) {
			return !mavenResourcesFiltering.filteredFileExtension(fileName, nonFilteredFileExtensions);
		}

		public boolean isFilteringDeploymentDescriptors() {
			return filteringDeploymentDescriptors;
		}

		public ArtifactFactory getArtifactFactory() {
			return this.artifactFactory;
		}

		public MavenSession getSession() {
			return session;
		}
	}

	public MavenProject getProject() {
		return project;
	}

	public void setProject(MavenProject project) {
		this.project = project;
	}

	public File getClassesDirectory() {
		return classesDirectory;
	}

	public void setClassesDirectory(File classesDirectory) {
		this.classesDirectory = classesDirectory;
	}

	public File getWebappDirectory() {
		return webappDirectory;
	}

	public void setWebappDirectory(File webappDirectory) {
		this.webappDirectory = webappDirectory;
	}

	public File getWarSourceDirectory() {
		return warSourceDirectory;
	}

	public void setWarSourceDirectory(File warSourceDirectory) {
		this.warSourceDirectory = warSourceDirectory;
	}

	public File getWebXml() {
		return webXml;
	}

	public void setWebXml(File webXml) {
		this.webXml = webXml;
	}

	public File getContainerConfigXML() {
		return containerConfigXML;
	}

	public void setContainerConfigXML(File containerConfigXML) {
		this.containerConfigXML = containerConfigXML;
	}

	public String getOutputFileNameMapping() {
		return outputFileNameMapping;
	}

	public void setOutputFileNameMapping(String outputFileNameMapping) {
		this.outputFileNameMapping = outputFileNameMapping;
	}

	public List getOverlays() {
		return overlays;
	}

	public void setOverlays(List overlays) {
		this.overlays = overlays;
	}

	public void addOverlay(Overlay overlay) {
		overlays.add(overlay);
	}

	public boolean isArchiveClasses() {
		return archiveClasses;
	}

	public void setArchiveClasses(boolean archiveClasses) {
		this.archiveClasses = archiveClasses;
	}

	public JarArchiver getJarArchiver() {
		return jarArchiver;
	}

	public void setJarArchiver(JarArchiver jarArchiver) {
		this.jarArchiver = jarArchiver;
	}

	public Resource[] getWebResources() {
		return webResources;
	}

	public void setWebResources(Resource[] webResources) {
		this.webResources = webResources;
	}

	public List getFilters() {
		return filters;
	}

	public void setFilters(List filters) {
		this.filters = filters;
	}

	public File getWorkDirectory() {
		return workDirectory;
	}

	public void setWorkDirectory(File workDirectory) {
		this.workDirectory = workDirectory;
	}

	public File getCacheFile() {
		return cacheFile;
	}

	public void setCacheFile(File cacheFile) {
		this.cacheFile = cacheFile;
	}

	public String getWarSourceIncludes() {
		return warSourceIncludes;
	}

	public void setWarSourceIncludes(String warSourceIncludes) {
		this.warSourceIncludes = warSourceIncludes;
	}

	public String getWarSourceExcludes() {
		return warSourceExcludes;
	}

	public void setWarSourceExcludes(String warSourceExcludes) {
		this.warSourceExcludes = warSourceExcludes;
	}

	public boolean isUseCache() {
		return useCache;
	}

	public void setUseCache(boolean useCache) {
		this.useCache = useCache;
	}

	public MavenArchiveConfiguration getArchive() {
		return archive;
	}

	public List getNonFilteredFileExtensions() {
		return nonFilteredFileExtensions;
	}

	public void setNonFilteredFileExtensions(List nonFilteredFileExtensions) {
		this.nonFilteredFileExtensions = nonFilteredFileExtensions;
	}

	public ArtifactFactory getArtifactFactory() {
		return this.artifactFactory;
	}

	public void setArtifactFactory(ArtifactFactory artifactFactory) {
		this.artifactFactory = artifactFactory;
	}

	protected MavenSession getSession() {
		return this.session;
	}
}
